﻿using System;
using System.Reflection;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Controls;
using wi = System.Windows.Interactivity;
using System.Windows.Media;
using System.ComponentModel;

namespace Thinktecture.UIAnnotations.Behaviors
{
    public class CommandBehavior : wi.TriggerAction<Control>
    {
        private string _eventName;
        private CommandedElementData _commandData;

        protected override void  OnAttached()
        {
            base.OnAttached();
            var enu = this.AssociatedObject.GetLocalValueEnumerator();
            while (enu.MoveNext())
            {
                var entry = (LocalValueEntry)enu.Current;
                if (entry.Property.Name == "ShadowTriggers")
                {
                    var triggerCol = entry.Value as wi.TriggerCollection;
                    foreach (var trigger in triggerCol)
                    {
                        var eventTrigger = trigger as wi.EventTrigger;
                        if (eventTrigger != null)
                        {
                            _eventName = eventTrigger.EventName;
                            return;
                        }
                    }
                }
            }
        }
        
        protected override void  OnDetaching()
        {
            this.RemoveHandler(this.AssociatedObject, _commandData); 
            base.OnDetaching();
        }

        protected override void Invoke(object parameter)
        {
        }

        private void CreateHandler(UIElement element, CommandedElementData elementData)
        {
            if (element == null || string.IsNullOrEmpty(elementData.EventName) || elementData.Command == null)
                return;
            if (elementData.Handler != null)
                RemoveHandler(element, elementData);

            // reflect the event
            EventInfo eventInfo = element.GetType().GetEvent(elementData.EventName);
            if (eventInfo == null)
                throw new InvalidOperationException(string.Format("The element \"{0}\" dosn't contain an event with the name \"{1}\"!", element.GetType().Name, elementData.EventName));
            Type handlerType = eventInfo.EventHandlerType;
            MethodInfo invokeMethod = handlerType.GetMethod("Invoke");
            ParameterInfo[] parms = invokeMethod.GetParameters();
            Type eventArgsType = parms[1].ParameterType;

            // create generic handler
            var genericHandlerType = typeof(GenericHandler<>).MakeGenericType(new Type[] { eventArgsType });
            var handler = Activator.CreateInstance(genericHandlerType, new object[] { elementData.Command, elementData.CommandParameter });// eventArgsType)
            var handlerMethod = handler.GetType().GetMethod("OnCompleted");
            var handlerDelegate = Delegate.CreateDelegate(eventInfo.EventHandlerType, handler, handlerMethod);

            // add handler
            eventInfo.AddEventHandler(element, handlerDelegate);
            elementData.Handler = new WeakReference(handlerDelegate);
        }

        private void RemoveHandler(UIElement element, CommandedElementData elementData)
        {
            if (element != null &&
                !string.IsNullOrEmpty(elementData.EventName) &&
                elementData.Handler != null &&
                elementData.Handler.IsAlive)
            {
                EventInfo eventInfo = element.GetType().GetEvent(elementData.EventName);
                eventInfo.RemoveEventHandler(element, (Delegate)elementData.Handler.Target);
            }
        }

        #region GetElementData

        private void GetElementData()
        {
            if (_commandData == null)
                _commandData = new CommandedElementData();
            if (string.IsNullOrEmpty(_eventName))
                return;
            _commandData.EventName = _eventName;
            return;
        }

        #endregion

        #region Command

        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandBehavior), new UIPropertyMetadata(null, OnCommandPropertyChanged));
        public ICommand Command { get { return (ICommand)GetValue(CommandProperty); } set { SetValue(CommandProperty, value); } }

        private static void OnCommandPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ((CommandBehavior)sender).OnCommandChanged(sender, e);
        }

        private void OnCommandChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var newCommand = e.NewValue as ICommand;
            var oldCommand = e.OldValue as ICommand;
            if (oldCommand != null)
                oldCommand.CanExecuteChanged -= this.OnCanExecuteChanged;

            if (newCommand == null)
                return;

            newCommand.CanExecuteChanged += this.OnCanExecuteChanged;

            if (string.IsNullOrEmpty(_eventName))
                return;

            this.GetElementData();
            _commandData.Command = newCommand;
            this.UpdateCanExecute();
            this.CreateHandler(this.AssociatedObject, _commandData);
        }

        private void OnCanExecuteChanged(object sender, EventArgs e)
        {
            if (!_raiseUpdate)
            {
                _raiseUpdate = true;
                this.UpdateCanExecute();
                _raiseUpdate = false;
            }
        }
        private bool _raiseUpdate = false;

        #endregion

        #region CommandParameter

        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.Register("CommandParameter", typeof(object), typeof(CommandBehavior), new PropertyMetadata(null));
        public object CommandParameter { get { return GetValue(CommandParameterProperty); } set { SetValue(CommandParameterProperty, value); } }

        #endregion

        #region PropertyName

        /// <summary>
        /// Dependency Property for PropertyName.
        /// </summary>
        public static readonly DependencyProperty PropertyNameProperty =
            DependencyProperty.Register("PropertyName", typeof(string), typeof(CommandBehavior),
            new PropertyMetadata("", OnPropertyNamePropertyChanged));
        public string PropertyName { get { return (string)GetValue(PropertyNameProperty); } set { SetValue(PropertyNameProperty, value); } }

        private static void OnPropertyNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((CommandBehavior)d).OnPropertyNameChanged(d, e);
        }
        
        private void OnPropertyNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            this.GetElementData();
            _commandData.PropertyName = (string)e.NewValue;
            if (_commandData.Handler == null)
            {
                this.CreateHandler(this.AssociatedObject, _commandData);
            }
        }

        #endregion

        #region CanExecutePropertyValue

        /// <summary>
        /// Dependency Property for CanExecutePropertyValue.
        /// </summary>
        public static readonly DependencyProperty CanExecutePropertyValueProperty =
            DependencyProperty.Register("CanExecutePropertyValue", typeof(object), typeof(CommandBehavior),
            new PropertyMetadata(null, OnCanExecutePropertyValuePropertyChanged));
        public object CanExecutePropertyValue { get { return GetValue(CanExecutePropertyValueProperty); } set { SetValue(CanExecutePropertyValueProperty, value); } }

        private static void OnCanExecutePropertyValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((CommandBehavior)d).OnCanExecutePropertyValueChanged(d, e);
        }

        private void OnCanExecutePropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            this.GetElementData();
            _commandData.CanExecutePropertyValue = e.NewValue;
            this.UpdateCanExecute();
        }

        #endregion

        #region CanNotExecutePropertyValue

        /// <summary>
        /// Dependency Property for CanNotExecutePropertyValue.
        /// </summary>
        public static readonly DependencyProperty CanNotExecutePropertyValueProperty =
            DependencyProperty.Register("CanNotExecutePropertyValue", typeof(object), typeof(CommandBehavior),
            new PropertyMetadata(null, OnCanNotExecutePropertyValuePropertyChanged));
        public object CanNotExecutePropertyValue { get { return GetValue(CanNotExecutePropertyValueProperty); } set { SetValue(CanNotExecutePropertyValueProperty, value); } }

        private static void OnCanNotExecutePropertyValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((CommandBehavior)d).OnCanNotExecutePropertyValueChanged(d, e);
        }

        private void OnCanNotExecutePropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            this.GetElementData();
            _commandData.CanNotExecutePropertyValue = e.NewValue;
            this.UpdateCanExecute();
        }
        
        #endregion

        #region Converter

        /// <summary>
        /// Dependency Property for Converter.
        /// </summary>
        public static readonly DependencyProperty ConverterProperty =
            DependencyProperty.Register("Converter", typeof(IValueConverter), typeof(CommandBehavior),
            new PropertyMetadata(null, OnConverterPropertyChanged));
        public IValueConverter Converter { get { return (IValueConverter)GetValue(ConverterProperty); } set { SetValue(ConverterProperty, value); } }

        private static void OnConverterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((CommandBehavior)d).OnConverterChanged(d, e);
        }

        private void OnConverterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            this.GetElementData();
            _commandData.Converter = (IValueConverter)e.NewValue;
            this.UpdateCanExecute();
        }
        
        #endregion

        #region ConverterParameter

        /// <summary>
        /// Dependency Property for ConverterParameter.
        /// </summary>
        public static readonly DependencyProperty ConverterParameterProperty =
            DependencyProperty.Register("ConverterParameter", typeof(object), typeof(CommandBehavior),
            new PropertyMetadata(null, OnConverterParameterPropertyChanged));
        public object ConverterParameter { get { return GetValue(ConverterParameterProperty); } set { SetValue(ConverterParameterProperty, value); } }

        private static void OnConverterParameterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((CommandBehavior)d).OnConverterParameterChanged(d, e);
        }

        private void OnConverterParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            this.GetElementData();
            _commandData.ConverterParameter = e.NewValue;
            this.UpdateCanExecute();
        }

        #endregion

        #region CommandAction

        /// <summary>
        /// Dependency Property for CommandAction.
        /// </summary>
        public static readonly DependencyProperty CommandActionProperty =
            DependencyProperty.Register("CommandAction", typeof(CommandAction), typeof(CommandBehavior),
            new PropertyMetadata(CommandAction.Disable, OnCommandActionPropertyChanged));
        public CommandAction CommandAction { get { return (CommandAction)GetValue(CommandActionProperty); } set { SetValue(CommandActionProperty, value); } }

        private static void OnCommandActionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((CommandBehavior)d).OnCommandActionChanged(d, e);
        }

        private void OnCommandActionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            this.GetElementData();
            _commandData.CommandAction = (CommandAction)e.NewValue;
            this.UpdateCanExecute();
        }

        #endregion
        
        #region UpdateCanExecute

        private void UpdateCanExecute()
        {
            var element = this.AssociatedObject;
            var data = _commandData;
            if (data == null || data.Command == null)
                return;

            // check execution state
            bool canExecute = (data.Command != null) && data.Command.CanExecute(data.CommandParameter);

            // apply command action
            if (data.CommandAction == CommandAction.Disable && element is Control)
            {
                ((Control)element).IsEnabled = canExecute;
            }
            else if (data.CommandAction == CommandAction.Hide && element is FrameworkElement)
            {
                ((FrameworkElement)element).Visibility = (canExecute) ? Visibility.Visible : Visibility.Collapsed;
            }

            if (string.IsNullOrEmpty(data.PropertyName))
                return;

            // assign property values
            PropertyInfo info = element.GetType().GetProperty(data.PropertyName);
            if (info != null)
            {
                try
                {
                    // if no property values present and property is bool, assign it directly
                    if (info.PropertyType.Equals(typeof(bool)) && data.CanExecutePropertyValue == null && data.CanNotExecutePropertyValue == null)
                    {
                        info.SetValue(element, canExecute, new object[] { });
                    }
                    else
                    {
                        // assign property values
                        object value;
                        if (canExecute)
                            value = ConvertValue(data.CanExecutePropertyValue, info.PropertyType, data.Converter, data.ConverterParameter);
                        else
                            value = ConvertValue(data.CanNotExecutePropertyValue, info.PropertyType, data.Converter, data.ConverterParameter);
                        info.SetValue(element, value, new object[] { });
                    }
                }
                catch { }
            }
        }

        private object ConvertValue(object propertyValue, Type targetType, IValueConverter converter, object converterParameter)
        {
            try
            {
                if (propertyValue.GetType().Equals(targetType))
                    return propertyValue;

                // use converter
                if (converter != null)
                {
                    return converter.Convert(propertyValue, targetType, converterParameter, System.Threading.Thread.CurrentThread.CurrentCulture);
                }
                else
                {
                    // try to use type converter via TypeConverterAttribute
                    var attribs = targetType.GetCustomAttributes(typeof(TypeConverterAttribute), true);
                    if (attribs != null && attribs.Length > 0)
                    {
                        var attrib = attribs[0] as TypeConverterAttribute;
                        Type converterType = Type.GetType(attrib.ConverterTypeName);
                        if (converterType != null)
                        {
                            TypeConverter typeConverter = Activator.CreateInstance<TypeConverter>();
                            if (typeConverter != null)
                            {
                                try
                                {
                                    var converted = typeConverter.ConvertTo(propertyValue, targetType);
                                    return converted;
                                }
                                catch { }
                            }
                        }
                    }

                    // try use BrushConverter
                    if (typeof(System.Windows.Media.Brush).IsAssignableFrom(targetType))
                    {
                        return new BrushConverter().Convert(propertyValue, targetType, null, System.Threading.Thread.CurrentThread.CurrentCulture);
                    }

                    // handle nullable types
                    if (targetType.IsGenericType &&
                        targetType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
                    {
                        Type originalType = targetType.GetGenericTypeDefinition();
                        Type t1 = targetType.GetGenericArguments()[0];
                        Type constructed = originalType.MakeGenericType(t1);
                        object value = null;
                        if (propertyValue != null)
                        {
                            value = Convert.ChangeType(propertyValue, t1, System.Threading.Thread.CurrentThread.CurrentCulture);
                            return Activator.CreateInstance(constructed, value);
                        }
                        return value;
                    }
                    else
                    {
                        // try standard convertation
                        return Convert.ChangeType(propertyValue, targetType, System.Threading.Thread.CurrentThread.CurrentCulture);
                    }
                }
            }
            catch { }
            return null;
        }

        #endregion

        #region CommandedElementData

        class CommandedElementData : IDisposable
        {
            public string EventName { get; set; }
            public ICommand Command { get; set; }
            public object CommandParameter { get; set; }
            public CommandAction CommandAction { get; set; }
            public string PropertyName { get; set; }
            public object CanExecutePropertyValue { get; set; }
            public object CanNotExecutePropertyValue { get; set; }
            public IValueConverter Converter { get; set; }
            public object ConverterParameter { get; set; }
            public WeakReference Handler { get; set; }

            #region IDisposable Members

            public void Dispose()
            {
                this.Command = null;
                this.CommandParameter = null;
                this.CanExecutePropertyValue = null;
                this.CanNotExecutePropertyValue = null;
                this.Converter = null;
                this.ConverterParameter = null;
                this.Handler = null;
            }

            #endregion
        }
        
        #endregion

        #region BrushConverter

        /// <summary>
        /// Converts a color string or rgb value to a SolidColorBrush instance and back.
        /// </summary>
        public class BrushConverter : IValueConverter
        {
            /// <summary>
            /// Modifies the source data before passing it to the target for display in the UI.
            /// </summary>
            /// <param name="value">The source data being passed to the target.</param>
            /// <param name="targetType">The System.Type of data expected by the target dependency property.</param>
            /// <param name="parameter">An optional parameter to be used in the converter logic.</param>
            /// <param name="culture">The culture of the conversion.</param>
            /// <returns>The value to be passed to the target dependency property.</returns>
            public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (value == null)
                {
                    return value;
                }
                string str = value as string;
                if (str != null)
                {
                    try
                    {
                        if (str.StartsWith("#"))
                        {
                            return new SolidColorBrush(ParseHexColor(value.ToString()));
                        }
                        else
                        {
                            var info = typeof(Colors).GetProperty(str);
                            if (info != null)
                            {
                                var color = info.GetValue(null, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, new object[] { }, System.Threading.Thread.CurrentThread.CurrentCulture);
                                if (color != null)
                                {
                                    return new SolidColorBrush((Color)color);
                                }
                            }
                        }
                    }
                    catch { }
                }
                return value;
            }

            /// <summary>
            /// Modifies the target data before passing it to the source object. This method is called only in System.Windows.Data.BindingMode.TwoWay bindings.
            /// </summary>
            /// <param name="value">The target data being passed to the source.</param>
            /// <param name="targetType">The System.Type of data expected by the source object.</param>
            /// <param name="parameter">An optional parameter to be used in the converter logic.</param>
            /// <param name="culture">The culture of the conversion.</param>
            /// <returns>The value to be passed to the source object.</returns>
            public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (value is Brush)
                {
                    Brush brush = (Brush)value;
                    if (value is string)
                    {
                        return brush.ToString();
                    }
                }
                return value;
            }

            private static Color ParseHexColor(string trimmedColor)
            {
                int num;
                int num2;
                int num3;
                int num4 = 0xff;
                if (trimmedColor.Length > 7)
                {
                    num4 = (ParseHexChar(trimmedColor[1]) * 0x10) + ParseHexChar(trimmedColor[2]);
                    num3 = (ParseHexChar(trimmedColor[3]) * 0x10) + ParseHexChar(trimmedColor[4]);
                    num2 = (ParseHexChar(trimmedColor[5]) * 0x10) + ParseHexChar(trimmedColor[6]);
                    num = (ParseHexChar(trimmedColor[7]) * 0x10) + ParseHexChar(trimmedColor[8]);
                }
                else if (trimmedColor.Length > 5)
                {
                    num3 = (ParseHexChar(trimmedColor[1]) * 0x10) + ParseHexChar(trimmedColor[2]);
                    num2 = (ParseHexChar(trimmedColor[3]) * 0x10) + ParseHexChar(trimmedColor[4]);
                    num = (ParseHexChar(trimmedColor[5]) * 0x10) + ParseHexChar(trimmedColor[6]);
                }
                else if (trimmedColor.Length > 4)
                {
                    num4 = ParseHexChar(trimmedColor[1]);
                    num4 += num4 * 0x10;
                    num3 = ParseHexChar(trimmedColor[2]);
                    num3 += num3 * 0x10;
                    num2 = ParseHexChar(trimmedColor[3]);
                    num2 += num2 * 0x10;
                    num = ParseHexChar(trimmedColor[4]);
                    num += num * 0x10;
                }
                else
                {
                    num3 = ParseHexChar(trimmedColor[1]);
                    num3 += num3 * 0x10;
                    num2 = ParseHexChar(trimmedColor[2]);
                    num2 += num2 * 0x10;
                    num = ParseHexChar(trimmedColor[3]);
                    num += num * 0x10;
                }
                return Color.FromArgb((byte)num4, (byte)num3, (byte)num2, (byte)num);
            }

            private static int ParseHexChar(char c)
            {
                int num = c;
                if ((num >= 0x30) && (num <= 0x39))
                {
                    return (num - 0x30);
                }
                if ((num >= 0x61) && (num <= 0x66))
                {
                    return ((num - 0x61) + 10);
                }
                if ((num < 0x41) || (num > 70))
                {
                    throw new FormatException("Illegal Token");
                }
                return ((num - 0x41) + 10);
            }
        }
        
        #endregion

    }
}
